package npr;

import processing.core.*;

import javax.media.opengl.*;
import com.sun.opengl.util.*;


public class DualMapShadow extends Shadow{

    /** The shadow maps (one for each light source) resulting from rendering the first layer of polygons from the light source position.*/
    protected RenderToTexture firstDepthShadowMaps[];
    /** The shadow maps (one for each light source) resulting from rendering the second layer of polygons from the light source position.*/
    protected RenderToTexture secondDepthShadowMaps[];
    /** The shadow maps (one for each light source) resulting from combining the first depth and the second depth shadow maps.*/
    protected RenderToTexture shadowMaps[];
    
    /** The size of the shadow map.*/
    protected int shadowMapSize;

    /** The camera matrix as seen from the light source.*/
    protected float[] lightCamera;
    /** The light's projection matrix.*/
     protected float[] lightProjection;

    /** A constant offset to avoid self-shadowing artifacts.*/
    protected float offset= 1.0f;

    /** Shader to combine first and second depth shadow maps.*/
    protected GLSL combineShadowMaps;

    /** Shader for linear depth distribution.*/
    protected GLSL linearDepth;

    /** Saves the renderer's far plane in order to restore it after rendering from the light source's position. */
    protected float farPlane;

    /** Have to save this matrix to restore the original state after rendering from the light source.*/
    protected PMatrix3D modelviewSaved, modelviewInvSaved, projectionSaved, cameraSaved, cameraInvSaved;

    /** The axis-aligned bounding box as seen from the camera's view. */
    protected PVector[] cameraBounds;

    /** Creates a DualMapShadow object and initializes it.*/
    public DualMapShadow(){
	
	renderer= (NPR)parent.g;
	gl= renderer.gl;

//         linearDepth=new GLSL();
//         linearDepth.loadVertexShader("linearDepthDistribution.vert");
//         linearDepth.useShaders();

	combineShadowMaps=new GLSL();
        combineShadowMaps.loadVertexShader("combineShadowMaps.vert");
        combineShadowMaps.loadFragmentShader("combineShadowMaps.frag");
        combineShadowMaps.useShaders();

        lightCamera= new float[16];
        lightProjection= new float[16];
	
	shadowMapSize= 1024;

	firstDepthShadowMaps= new RenderToTexture[renderer.MAX_LIGHTS];
	secondDepthShadowMaps= new RenderToTexture[renderer.MAX_LIGHTS];
	shadowMaps= new RenderToTexture[renderer.MAX_LIGHTS];

	for(int i= 0; i<renderer.MAX_LIGHTS; i++){
	    firstDepthShadowMaps[i]= new RenderToTexture(shadowMapSize, shadowMapSize);
	    secondDepthShadowMaps[i]= new RenderToTexture(shadowMapSize, shadowMapSize);
	    shadowMaps[i]= new RenderToTexture(shadowMapSize, shadowMapSize);
	}

	cameraBounds= new PVector[8];
	for(int i=0; i<8; i++){
	    cameraBounds[i]= new PVector();
	}
    }
    
    /**
     * Sets the shadow map size. As a rule of thumb, the shadow map size should be at least twice the size of
     * your screen width/height. For best performance, use a power of two size.
     *
     * @param size the shadow map size.
     */
    public void setMapSize(int size){

	shadowMapSize= size;
	for(int i= 0; i<renderer.MAX_LIGHTS; i++){
	    firstDepthShadowMaps[i]= new RenderToTexture(shadowMapSize, shadowMapSize);
	    secondDepthShadowMaps[i]= new RenderToTexture(shadowMapSize, shadowMapSize);
	    shadowMaps[i]= new RenderToTexture(shadowMapSize, shadowMapSize);
	}
    }
    
    /**
    * Dual shadow maps use a constant offset to avoid self-shadowing artifacts. In most cases the default
    * offset should be sufficient.
    * 
    *@param offset a constant offset
    */
    public void setOffset(float offset){
	this.offset= offset;
    }

    /** 
     * Renders and combines shadow maps. You don't have to call this, it's called by the renderer.
     */
    public void preProcess(){
	for(int light=0; light<renderer.lightCount; light++){
	    
	    renderShadowMap(light, false);    
	    
	    renderShadowMap(light, true);
	   
	    shadowMaps[light].begin();
	    combineShadowMaps.startShader();

	    int location;

	    gl.glActiveTexture(GL.GL_TEXTURE0);
	    firstDepthShadowMaps[light].bindDepthTexture();
	    location= combineShadowMaps.getUniformLocation("firstDepthShadowMap");
	    combineShadowMaps.setTextureUnit(location, 0);

	    gl.glActiveTexture(GL.GL_TEXTURE1);
	    secondDepthShadowMaps[light].bindDepthTexture();
	    location= combineShadowMaps.getUniformLocation("secondDepthShadowMap");
	    combineShadowMaps.setTextureUnit(location, 1);

	    location= combineShadowMaps.getUniformLocation("textureSize");
	    combineShadowMaps.setFloat(location, (float)firstDepthShadowMaps[light].getWidth());

	    location= combineShadowMaps.getUniformLocation("offset");
	    combineShadowMaps.setFloat(location, offset);

	    renderer.fullScreenQuad(-1.0f);

	    combineShadowMaps.endShader();
	    shadowMaps[light].end();
	    
	    gl.glActiveTexture(GL.GL_TEXTURE1);
	    secondDepthShadowMaps[light].unbind();

	    gl.glActiveTexture(GL.GL_TEXTURE0);
	    firstDepthShadowMaps[light].unbind();

	    initShadowMap(light);
	}
    }

    /** renders a shadow map from the light source's position.
     * 
     * @param light the light source index
     * @param secondDepth states whether to render the first or second depth layer as seen from the light source.
     */
    protected void renderShadowMap(int light, boolean secondDepth){
		
	saveMatrices();

	renderer= (NPR)parent.g;
	gl= renderer.gl;
             
	PVector up= new PVector();
	PVector lightDirection=  renderer.lightNormalWorld[light];
        
	if(lightDirection.y==0 && lightDirection.z==0){
	   up.x= 0;
	   up.y= 1;
	   up.z= 0;
	}
	else{
	    up.x=0;
	    up.y= lightDirection.z;
	    up.z= -lightDirection.y;
	}
	
	up.normalize();

	PVector lightPosition;
	if(renderer.lightType[light]==renderer.DIRECTIONAL){
	    lightPosition= PVector.div(lightDirection, -0.001f);
	}
	else{
	    lightPosition=  renderer.lightPositionWorld[light];
	}

	//System.out.println(lightPosition);
	
	renderer.camera(lightPosition.x, lightPosition.y, lightPosition.z, 0, 0, 0, up.x, up.y, up.z);
	
	renderer.transpose(renderer.camera, lightCamera);

	gl.glMatrixMode(GL.GL_MODELVIEW);
        gl.glPushMatrix();
	gl.glMultMatrixf(lightCamera, 0);

	PVector[] worldBounds;

	// calculate axis aligned bounding box in camera space
	worldBounds= renderer.getBoundingBox(start);
	    
	renderer.camera.mult(worldBounds[0], cameraBounds[0]);

	float left= cameraBounds[0].x;
	float right= cameraBounds[0].x;
	float bottom= cameraBounds[0].y;
	float top= cameraBounds[0].y;
	float near= cameraBounds[0].z;
	float far= cameraBounds[0].z;

	for(int i= start; i<stop; i++){
	    
	    worldBounds= renderer.getBoundingBox(i);
	    
	    for(int k=0; k<8; k++){
		renderer.camera.mult(worldBounds[k], cameraBounds[k]);
		
		if (cameraBounds[k].x < left) left=cameraBounds[k].x;
		if (cameraBounds[k].x > right) right=cameraBounds[k].x;
		if (cameraBounds[k].y < bottom) bottom=cameraBounds[k].y;
		if (cameraBounds[k].y > top) top=cameraBounds[k].y;
		if (cameraBounds[k].z > near) near=cameraBounds[k].z;
		if (cameraBounds[k].z < far) far=cameraBounds[k].z;
		
	    }
	}
	
	float lightDistance= 0.0f;
	if(renderer.lightType[light]==renderer.DIRECTIONAL){ 
	    lightDistance= lightPosition.mag();
	    renderer.ortho(left, right, bottom, top, -near, -far);
// 	    System.out.println("left: " + left + " right: " + right +
// 	    			   " bottom: " + bottom + " top: " + top +
// 	    			   " near: " + (-near-lightDistance) + " far: " +  (-far-lightDistance));
	}
	else{
	    renderer.frustum(left, right, bottom, top, -near, -far);
//  	    System.out.println("left: " + left + " right: " + right +
//  			       " bottom: " + bottom + " top: " + top +
//  			       " near: " + -near + " far: " + -far);
	}

        renderer.transpose(renderer.projection, lightProjection);
	
// 	if(renderer.lightType[light]==renderer.DIRECTIONAL){
// 	    lightProjection[10]/=(-far-lightDistance);
// 	    lightProjection[14]/=(-far-lightDistance); 
// 	}
// 	else{
// 	    lightProjection[10]/=(-far);
// 	    lightProjection[14]/=(-far);
// 	}

// 	for(int i=0; i<16; i++){
// 	    System.out.print(lightProjection[i]+" ");
// 	}
// 	System.out.println();
	
 	gl.glMatrixMode(GL.GL_PROJECTION);
        gl.glPushMatrix();
	gl.glLoadMatrixf(lightProjection, 0);
	
	int[] modelTarget= {GL.GL_MODELVIEW};

	gl.glMatrixMode(GL.GL_MODELVIEW);

	if(secondDepth) secondDepthShadowMaps[light].begin();
	else firstDepthShadowMaps[light].begin();

	//we are only interested in depth values here
	gl.glShadeModel(GL.GL_FLAT);
	// lighting is disabled by default
	//gl.glDisable(GL.GL_LIGHTING);
	gl.glDisable(GL.GL_NORMALIZE);
	gl.glDisable(GL.GL_COLOR_MATERIAL);
	gl.glColorMask(false, false, false, false);
	gl.glEnable(GL.GL_CULL_FACE);
	
	if(secondDepth) gl.glCullFace(GL.GL_FRONT);
	else gl.glCullFace(GL.GL_BACK);

	//linearDepth.startShader();
	renderer.render(start, stop, null, modelTarget, null, null); 
        //linearDepth.endShader();

	// restore normal drawing state
	gl.glShadeModel(GL.GL_SMOOTH);
	//gl.glEnable(GL.GL_LIGHTING);
	gl.glEnable(GL.GL_COLOR_MATERIAL);
	gl.glEnable(GL.GL_NORMALIZE);
	gl.glColorMask(true, true, true, true);
	gl.glDisable(GL.GL_POLYGON_OFFSET_FILL);
	gl.glEnable(GL.GL_CULL_FACE);
	gl.glCullFace(GL.GL_BACK);

	if(secondDepth) secondDepthShadowMaps[light].end();
	else firstDepthShadowMaps[light].end();

        gl.glMatrixMode(GL.GL_PROJECTION);
        gl.glPopMatrix();
        gl.glMatrixMode(GL.GL_MODELVIEW);
        gl.glPopMatrix();
        gl.glViewport(0, 0, parent.width, parent.height);

	gl.glClear(GL.GL_DEPTH_BUFFER_BIT | GL.GL_COLOR_BUFFER_BIT);
	restoreMatrices();
    }

    /** 
     * Initializes a shodow map. 
     *
     *@param light light source index
     */
    protected void initShadowMap(int light){

	gl.glMatrixMode(GL.GL_MODELVIEW);
        gl.glPushMatrix();
	gl.glMultMatrixf(lightCamera, 0);

	gl.glActiveTexture(renderer.textureUnits[light+1]);
	gl.glEnable(GL.GL_TEXTURE_2D);
        shadowMaps[light].bindDepthTexture();        
	
        gl.glEnable(GL.GL_TEXTURE_GEN_S);                           
        gl.glEnable(GL.GL_TEXTURE_GEN_T);                           
        gl.glEnable(GL.GL_TEXTURE_GEN_R);                           
        gl.glEnable(GL.GL_TEXTURE_GEN_Q);  
	
	gl.glTexGeni(GL.GL_S, GL.GL_TEXTURE_GEN_MODE, GL.GL_EYE_LINEAR);
        gl.glTexGeni(GL.GL_T, GL.GL_TEXTURE_GEN_MODE, GL.GL_EYE_LINEAR);
        gl.glTexGeni(GL.GL_R, GL.GL_TEXTURE_GEN_MODE, GL.GL_EYE_LINEAR);
        gl.glTexGeni(GL.GL_Q, GL.GL_TEXTURE_GEN_MODE, GL.GL_EYE_LINEAR);
	
        gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, GL.GL_CLAMP_TO_BORDER);
        gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_CLAMP_TO_BORDER);
        gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR);
        gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR); 
	gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_DEPTH_TEXTURE_MODE, GL.GL_LUMINANCE);
        gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_COMPARE_MODE, GL.GL_COMPARE_R_TO_TEXTURE_ARB);
        gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_COMPARE_FUNC, GL.GL_LEQUAL);
        
        gl.glMatrixMode(GL.GL_TEXTURE);    
        
	gl.glLoadIdentity();
        gl.glTranslatef(0.5f, 0.5f, 0.5f);
        gl.glScalef(0.5f, 0.5f, 0.5f);
	gl.glMultMatrixf(lightProjection, 0);
	gl.glScalef(1.0f, -1.0f, 1.0f);
	gl.glMultMatrixf(lightCamera, 0);
	
        gl.glDisable(GL.GL_TEXTURE_GEN_S);                           
        gl.glDisable(GL.GL_TEXTURE_GEN_T);                           
        gl.glDisable(GL.GL_TEXTURE_GEN_R);                           
        gl.glDisable(GL.GL_TEXTURE_GEN_Q);        
	     
	shadowMaps[light].unbind();
	gl.glDisable(GL.GL_TEXTURE_2D);
	gl.glActiveTexture(GL.GL_TEXTURE0);

	gl.glMatrixMode(GL.GL_MODELVIEW);
        gl.glPopMatrix();
    }

    /** Saves Processing matrices before rendering from light source's position. */
    protected void saveMatrices(){
	modelviewSaved= renderer.modelview.get();
	modelviewInvSaved= renderer.modelviewInv.get();
	projectionSaved= renderer.projection.get();
	cameraSaved= renderer.camera.get();
	cameraInvSaved= renderer.getCameraInv();
	this.farPlane= renderer.farPlane;
    }

    /** Restores Processing matrices after rendering from light source's position. */
    protected void restoreMatrices(){
	renderer.modelview.set(modelviewSaved);
	renderer.modelviewInv.set(modelviewInvSaved);
	renderer.projection.set(projectionSaved);
	renderer.camera.set(cameraSaved);
	renderer.setCameraInv(cameraInvSaved);
	renderer.farPlane= this.farPlane;
    }

    /** Sets the state of the main shader. */
    protected void prepare(){

	renderer= (NPR)parent.g;
	gl= renderer.gl;

	renderer.setBoolean("mapShadow", true);
	renderer.setFloat("shadowTransparency", brightness);
	renderer.setInteger("lightCountInt", renderer.lightCount);
	renderer.setFloat("lightCountFloat", (float)renderer.lightCount);
	for(int i= 0; i< renderer.lightCount; i++){
	    renderer.setInteger("lightType[" + i + "]", renderer.lightType[i]);
	}
	
	// if more lights are set than available texture units-1 (the first one is reserved for cel shading) throw an exception
	if(renderer.lightCount>renderer.textureUnits.length-1){
	    throw new RuntimeException("not enough texture units (available: " + (renderer.textureUnits.length-1) + "). Decrease number of lights.");
	}
	for(int light=0; light<renderer.lightCount; light++){
	    gl.glActiveTexture(renderer.textureUnits[light+1]);
	    gl.glEnable(GL.GL_TEXTURE_2D);
	    shadowMaps[light].bindDepthTexture();
	}
    }

    /**
     * Resets the OpenGL state set in prepare()
     */
    protected void cleanUp(){

	renderer= (NPR)parent.g;
	gl= renderer.gl;
	
	for(int light=0; light<renderer.lightCount; light++){
	    gl.glActiveTexture(renderer.textureUnits[light+1]);
	    shadowMaps[light].unbind();
	}
        gl.glDisable(GL.GL_TEXTURE_2D);
	gl.glActiveTexture(GL.GL_TEXTURE0);
    }
    
    /**
     * Test routine. Uncomment to see shadow maps.
     */
    
    public void postProcess(){
	/*
	renderer= (NPR)parent.g;
	gl= renderer.gl;

	gl.glActiveTexture(GL.GL_TEXTURE0);
	gl.glEnable(GL.GL_TEXTURE_2D);
        shadowMaps[0].bindDepthTexture();
	gl.glColor3f(1.0f, 1.0f, 1.0f);
	renderer.fullScreenQuad(-1.0f);
	shadowMaps[0].unbind();
	gl.glDisable(GL.GL_TEXTURE_2D);
	*/      
    } 
}
